const moment = require('moment')
const ResponseError = require('../ResponseError')
const {
  PAGINATION_LIMIT_DEFAULT,
} = require('../../config')

const METHODS = {
  $or: 'orWhere',
  $ne: 'whereNot',
  $in: 'whereIn',
  $nin: 'whereNotIn',
}

const OPERATORS = {
  $lt: '<',
  $lte: '<=',
  $gt: '>',
  $gte: '>=',
  $like: 'like',
  $ilike: 'ilike',
  $equal: '=',
}

// helper method
// turn { name: 'Binatir' } to { name: { '$equal': '%Binatir%' } }
const changeValueToValueObject = value => (
  typeof value !== 'object' ? { $equal: value } : value
)

// helper method
const addFilterQuery = (query, method, item, itemKey) => {
  const itemValue = item[itemKey]
  if (itemValue) {
    const itemValueObject = changeValueToValueObject(itemValue)
    Object.keys(itemValueObject).forEach((itemValueObjectKey) => {
      const operator = OPERATORS[itemValueObjectKey] || '='
      const value = itemValueObject[itemValueObjectKey]
      query[method](itemKey, operator, value)
    })
  }
}

function KnexService(knexClient) {
  this.knexClient = knexClient

  this.find = async (tableName, params = {}) => {
    const {
      $limit,
      $offset,
      $sort,
      $select,
      $join = [],
      ...otherParams
    } = params
    const countQuery = this.knexClient(tableName).count('* as total')
    const findQuery = this.knexClient(tableName)
    const limit = +($limit || PAGINATION_LIMIT_DEFAULT)
    findQuery.limit(limit)
    const offset = +($offset || 0)
    findQuery.offset(offset)
    const select = $select || '*'
    findQuery.select(select)
    const sort = $sort
    if (sort) {
      Object.keys(sort).forEach((key) => {
        const order = +(sort[key]) === 1 ? 'asc' : 'desc'
        findQuery.orderBy(key, order)
      })
    }
    const where = otherParams || {}
    Object.keys(where).forEach((key) => {
      // Option 1:
      // { name: 'Binatir' }
      // Option 2:
      // { name: { '$like': '%Binatir%' } }
      // Option 3:
      // { $or: [ { name: { '$like': '%Binatir%' } } ] }
      // { $or: { name: { '$like': '%Binatir%' } } }
      const method = METHODS[key]
      if (method) {
        // { $or: [ { name: { '$like': '%Binatir%' } } ] }
        const paramValue = where[key]
        if (paramValue) {
          const items = [].concat(paramValue)
          items.forEach((item) => {
            Object.keys(item).forEach((itemKey) => {
              addFilterQuery(countQuery, method, item, itemKey)
              addFilterQuery(findQuery, method, item, itemKey)
            })
          })
        }
      } else {
        // { name: { '$like': '%Binatir%' } }
        const item = where
        const itemKey = key
        addFilterQuery(countQuery, 'where', item, itemKey)
        addFilterQuery(findQuery, 'where', item, itemKey)
      }
    })
    const countResult = await countQuery
    const { total } = countResult[0]
    let findResult = await findQuery
    // $join: [{
    //   tableName: 'administrator'
    //   foreignKey: 'administrator_id',
    //   tableKey: 'id',
    // }]
    if (findResult.length > 0 && $join.length > 0) {
      for (let i = 0; i < $join.length; i += 1) {
        const {
          tableName: joinTableName, // organisation
          foreignKey: joinForeignKey, // organisationId in user
          tableKey: joinTableKey, // id in organisation
        } = $join[i]
        // foreign key values from the find result
        const foreignKeys = findResult.map(item => item[joinForeignKey])
        // select result from joined table with selected foreign key values
        const joinItems = await this.knexClient(joinTableName).select('*').whereIn(joinTableKey, foreignKeys)
        const joinItemMap = joinItems.reduce((prev, current) => {
          const { id } = current
          return {
            ...prev,
            [id]: current,
          }
        }, {})
        findResult = findResult.map(item => ({
          ...item,
          [joinTableName]: joinItemMap[item[joinForeignKey]],
        }))
      }
    }

    return {
      total,
      limit,
      offset,
      data: findResult,
    }
  }

  this.get = async (tableName, id, params = {}) => {
    const result = await this.find(tableName, { ...params, id })
    const record = result.data[0]
    if (!record) {
      return Promise.reject(new ResponseError.NotFound())
    }
    return record
  }

  this.create = async (tableName, params = {}) => {
    const createParams = params
    delete createParams.created_at
    delete createParams.id
    createParams.updated_at = moment().format('YYYY-MM-DD H:mm:ss')
    const ids = await this.knexClient(tableName).insert(createParams)
    const id = ids[0]
    const record = await this.get(tableName, id)
    return record
  }

  this.update = async (tableName, id, params = {}) => {
    const updateParams = params
    delete updateParams.created_at
    delete updateParams.id
    updateParams.updated_at = moment().format('YYYY-MM-DD H:mm:ss')
    await this.knexClient(tableName).where('id', id).update(updateParams)
    const record = await this.get(tableName, id)
    return record
  }

  this.remove = async (tableName, id) => {
    const success = await this.knexClient(tableName).where('id', id).del()
    if (!success) {
      return Promise.reject(new ResponseError.NotFound())
    }
    return success
  }
}

module.exports = KnexService
